/* ------------------------------------------------------------------
 * node-onvif - device.js
 *
 * Copyright (c) 2016-2018, Futomi Hatano, All rights reserved.
 * Released under the MIT license
 * Date: 2018-08-13
 * ---------------------------------------------------------------- */
'use strict'
const mCrypto = require('crypto')
const mUrl = require('url')
const mUtil = require('util')
const mEventEmitter = require('events').EventEmitter

const mOnvifServiceDevice = require('./service-device.js')
const mOnvifServiceMedia = require('./service-media.js')
const mOnvifServicePtz = require('./service-ptz.js')
const mOnvifServiceEvents = require('./service-events.js')
const mOnvifHttpAuth = require('./http-auth.js')

/* ------------------------------------------------------------------
 * Constructor: OnvifDevice(params)
 * - params:
 *    - address : IP address of the targeted device
 *                (Required if the `xaddr` is not specified)
 *    - xaddr   : URL of the entry point for the device management service
 *                (Required if the `address' is not specified)
 *                If the `xaddr` is specified, the `address` is ignored.
 *    - user  : User name (Optional)
 *    - pass  : Password (Optional)
 * ---------------------------------------------------------------- */
function OnvifDevice(params) {
  if (!params || typeof params !== 'object') {
    throw new Error('The parameter was invalid.')
  }

  this.address = ''
  this.xaddr = ''
  this.user = ''
  this.pass = ''
  this.keepAddr = false
  this.lastResponse = null // for debug

  if ('xaddr' in params && typeof params['xaddr'] === 'string') {
    this.xaddr = params['xaddr']
    let ourl = mUrl.parse(this.xaddr)
    this.address = ourl.hostname
  } else if ('address' in params && typeof params['address'] === 'string') {
    this.keepAddr = true
    this.address = params['address']
    this.xaddr = 'http://' + this.address + '/onvif/device_service'
  } else {
    throw new Error('The parameter was invalid.')
  }
  if ('user' in params && typeof params['user'] === 'string') {
    this.user = params['user'] || ''
  }
  if ('pass' in params && typeof params['pass'] === 'string') {
    this.pass = params['pass'] || ''
  }

  this.oxaddr = mUrl.parse(this.xaddr)
  if (this.user) {
    this.oxaddr.auth = this.user + ':' + this.pass
  }

  this.time_diff = 0

  this.information = null
  this.services = {
    device: new mOnvifServiceDevice({
      xaddr: this.xaddr,
      user: this.user,
      pass: this.pass
    }),
    events: null,
    imaging: null,
    media: null,
    ptz: null
  }
  this.profile_list = []

  this.current_profile = null
  this.ptz_moving = false

  mEventEmitter.call(this)
}
mUtil.inherits(OnvifDevice, mEventEmitter)

OnvifDevice.prototype._isValidCallback = function (callback) {
  return callback && typeof callback === 'function' ? true : false
}

OnvifDevice.prototype._execCallback = function (callback, arg1, arg2) {
  if (this._isValidCallback(callback)) {
    callback(arg1, arg2)
  }
}

/* ------------------------------------------------------------------
 * Method: getInformation()
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.getInformation = function () {
  let o = this.information
  if (o) {
    return JSON.parse(JSON.stringify(o))
  } else {
    return null
  }
}

/* ------------------------------------------------------------------
 * Method: getCurrentProfile()
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.getCurrentProfile = function () {
  let o = this.current_profile
  if (o) {
    return JSON.parse(JSON.stringify(o))
  } else {
    return null
  }
}

/* ------------------------------------------------------------------
 * Method: getProfileList()
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.getProfileList = function () {
  return JSON.parse(JSON.stringify(this.profile_list))
}

/* ------------------------------------------------------------------
 * Method: changeProfile(index|token)
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.changeProfile = function (index) {
  if (typeof index === 'number' && index >= 0 && index % 1 === 0) {
    let p = this.profile_list[index]
    if (p) {
      this.current_profile = p
      return this.getCurrentProfile()
    } else {
      return null
    }
  } else if (typeof index === 'string' && index.length > 0) {
    let new_profile = null
    for (let i = 0; i < this.profile_list.length; i++) {
      if (this.profile_list[i]['token'] === index) {
        new_profile = this.profile_list[i]
        break
      }
    }
    if (new_profile) {
      this.current_profile = new_profile
      return this.getCurrentProfile()
    } else {
      return null
    }
  } else {
    return null
  }
}

/* ------------------------------------------------------------------
 * Method: getUdpStreamUrl()
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.getUdpStreamUrl = function () {
  if (!this.current_profile) {
    return ''
  }
  let url = this.current_profile['stream']['udp']
  return url ? url : ''
}

/* ------------------------------------------------------------------
 * Method: fetchSnapshot()
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.fetchSnapshot = function (callback) {
  let promise = new Promise((resolve, reject) => {
    if (!this.current_profile) {
      reject(new Error('No media profile is selected.'))
      return
    }
    if (!this.current_profile['snapshot']) {
      reject(
        new Error(
          'The device does not support snapshot or you have not authorized by the device.'
        )
      )
      return
    }
    let ourl = mUrl.parse(this.current_profile['snapshot'])
    let options = {
      protocol: ourl.protocol,
      auth: this.user + ':' + this.pass,
      hostname: ourl.hostname,
      port: ourl.port || 80,
      path: ourl.path,
      method: 'GET'
    }
    let req = mOnvifHttpAuth.request(options, (res) => {
      let buffer_list = []
      res.on('data', (buf) => {
        buffer_list.push(buf)
      })
      res.on('end', () => {
        if (res.statusCode === 200) {
          let buffer = Buffer.concat(buffer_list)
          let ct = res.headers['content-type']
          if (!ct) {
            // workaround for DBPOWER
            ct = 'image/jpeg'
          }
          if (ct.match(/image\//)) {
            resolve({ headers: res.headers, body: buffer })
          } else if (ct.match(/^text\//)) {
            reject(new Error(buffer.toString()))
          } else {
            reject(new Error('Unexpected data: ' + ct))
          }
        } else {
          reject(new Error(res.statusCode + ' ' + res.statusMessage))
        }
      })
      req.on('error', (error) => {
        reject(error)
      })
    })
    req.on('error', (error) => {
      reject(error)
    })
    req.end()
  })
  if (this._isValidCallback(callback)) {
    promise
      .then((res) => {
        callback(null, res)
      })
      .catch((error) => {
        callback(error)
      })
  } else {
    return promise
  }
}

/* ------------------------------------------------------------------
 * Method: ptzMove(params[, callback])
 * - params:
 *   - speed:
 *     - x     | Float   | required | speed for pan (in the range of -1.0 to 1.0)
 *     - y     | Float   | required | speed for tilt (in the range of -1.0 to 1.0)
 *     - z     | Float   | required | speed for zoom (in the range of -1.0 to 1.0)
 *   - timeout | Integer | optional | seconds (Default 1)
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.ptzMove = function (params, callback) {
  let promise = new Promise((resolve, reject) => {
    if (!this.current_profile) {
      reject(new Error('No media profile is selected.'))
      return
    }
    if (!this.services['ptz']) {
      reject(new Error('The device does not support PTZ.'))
      return
    }

    let speed = params['speed']
    if (!speed) {
      speed = {}
    }
    let x = speed['x'] || 0
    let y = speed['y'] || 0
    let z = speed['z'] || 0

    let timeout = params['timeout']
    if (!timeout || typeof timeout !== 'number') {
      timeout = 1
    }
    let p = {
      ProfileToken: this.current_profile['token'],
      Velocity: { x: x, y: y, z: z },
      Timeout: timeout
    }
    this.ptz_moving = true
    this.services['ptz']
      .continuousMove(p)
      .then(() => {
        resolve()
      })
      .catch((error) => {
        reject(error)
      })
  })
  if (this._isValidCallback(callback)) {
    promise
      .then(() => {
        callback(null)
      })
      .catch((error) => {
        callback(error)
      })
  } else {
    return promise
  }
}

/* ------------------------------------------------------------------
 * Method: ptzStop([callback])
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.ptzStop = function (callback) {
  let promise = new Promise((resolve, reject) => {
    if (!this.current_profile) {
      reject(new Error('No media profile is selected.'))
      return
    }
    if (!this.services['ptz']) {
      reject(new Error('The device does not support PTZ.'))
      return
    }
    this.ptz_moving = false
    let p = {
      ProfileToken: this.current_profile['token'],
      PanTilt: true,
      Zoom: true
    }
    this.services['ptz']
      .stop(p)
      .then((result) => {
        resolve(result)
      })
      .catch((error) => {
        reject(error)
      })
  })
  if (this._isValidCallback(callback)) {
    promise
      .then((res) => {
        callback(null, res)
      })
      .catch((error) => {
        callback(error)
      })
  } else {
    return promise
  }
}

/* ------------------------------------------------------------------
 * Method: setAuth(user, pass)
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.setAuth = function (user, pass) {
  this.user = user || ''
  this.pass = pass || ''
  if (this.user) {
    this.oxaddr.auth = this.user + ':' + this.pass
  }
  for (let k in this.services) {
    let s = this.services[k]
    if (s) {
      this.services[k].setAuth(user, pass)
    }
  }
}

/* ------------------------------------------------------------------
 * Method: init([callback])
 * ---------------------------------------------------------------- */
OnvifDevice.prototype.init = function (callback) {
  let promise = new Promise((resolve, reject) => {
    this._getSystemDateAndTime()
      .then(() => {
        return this._getCapabilities()
      })
      .then(() => {
        return this._getDeviceInformation()
      })
      .then(() => {
        return this._mediaGetProfiles()
      })
      .then(() => {
        return this._mediaGetStreamURI()
      })
      .then(() => {
        return this._mediaGetSnapshotUri()
      })
      .then(() => {
        let info = this.getInformation()
        resolve(info)
      })
      .catch((error) => {
        reject(error)
      })
  })
  if (this._isValidCallback(callback)) {
    promise
      .then((info) => {
        callback(null, info)
      })
      .catch((error) => {
        callback(error)
      })
  } else {
    return promise
  }
}

// GetSystemDateAndTime (Access Class: PRE_AUTH)
OnvifDevice.prototype._getSystemDateAndTime = function () {
  let promise = new Promise((resolve, reject) => {
    this.services.device.getSystemDateAndTime((error, result) => {
      // Ignore the error becase some devices do not support
      // the GetSystemDateAndTime command and the error does
      // not cause any trouble.
      if (!error) {
        this.time_diff = this.services.device.getTimeDiff()
      }
      resolve()
    })
  })
  return promise
}

// GetCapabilities (Access Class: PRE_AUTH)
OnvifDevice.prototype._getCapabilities = function () {
  let promise = new Promise((resolve, reject) => {
    this.services.device.getCapabilities((error, result) => {
      this.lastResponse = result
      if (error) {
        reject(
          new Error('Failed to initialize the device: ' + error.toString())
        )
        return
      }
      let c = result['data']['GetCapabilitiesResponse']['Capabilities']
      if (!c) {
        reject(
          new Error(
            'Failed to initialize the device: No capabilities were found.'
          )
        )
        return
      }
      let events = c['Events']
      if (events && events['XAddr']) {
        this.services.events = new mOnvifServiceEvents({
          xaddr: this._getXaddr(events['XAddr']),
          time_diff: this.time_diff,
          user: this.user,
          pass: this.pass
        })
      }
      let imaging = c['Imaging']
      if (imaging && imaging['XAddr']) {
        /*
				this.services.imaging = new mOnvifServiceImaging({
					'xaddr'    : imaging['XAddr'],
					'time_diff': this.time_diff,
					'user'     : this.user,
					'pass'     : this.pass
				});
				*/
      }
      let media = c['Media']
      if (media && media['XAddr']) {
        this.services.media = new mOnvifServiceMedia({
          xaddr: this._getXaddr(media['XAddr']),
          time_diff: this.time_diff,
          user: this.user,
          pass: this.pass
        })
      }
      let ptz = c['PTZ']
      if (ptz && ptz['XAddr']) {
        this.services.ptz = new mOnvifServicePtz({
          xaddr: this._getXaddr(ptz['XAddr']),
          time_diff: this.time_diff,
          user: this.user,
          pass: this.pass
        })
      }
      resolve()
    })
  })
  return promise
}

// GetDeviceInformation (Access Class: READ_SYSTEM)
OnvifDevice.prototype._getDeviceInformation = function () {
  let promise = new Promise((resolve, reject) => {
    this.services.device.getDeviceInformation((error, result) => {
      if (error) {
        reject(
          new Error('Failed to initialize the device: ' + error.toString())
        )
      } else {
        this.information = result['data']['GetDeviceInformationResponse']
        resolve()
      }
    })
  })
  return promise
}

// Media::GetProfiles (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetProfiles = function () {
  let promise = new Promise((resolve, reject) => {
    this.services.media.getProfiles((error, result) => {
      this.lastResponse = result
      if (error) {
        reject(
          new Error('Failed to initialize the device: ' + error.toString())
        )
        return
      }
      let profiles = result['data']['GetProfilesResponse']['Profiles']
      if (!profiles) {
        reject(
          new Error(
            'Failed to initialize the device: The targeted device does not any media profiles.'
          )
        )
        return
      }
      profiles = [].concat(profiles) // in case profiles is not a list, then forEach below will report an error
      profiles.forEach((p) => {
        let profile = {
          token: p['$']['token'],
          name: p['Name'],
          snapshot: '',
          stream: {
            udp: '',
            http: '',
            rtsp: ''
          },
          video: {
            source: null,
            encoder: null
          },
          audio: {
            source: null,
            encoder: null
          },
          ptz: {
            range: {
              x: {
                min: 0,
                max: 0
              },
              y: {
                min: 0,
                max: 0
              },
              z: {
                min: 0,
                max: 0
              }
            }
          }
        }

        if (p['VideoSourceConfiguration']) {
          profile['video']['source'] = {
            token: p['VideoSourceConfiguration']['$']['token'],
            name: p['VideoSourceConfiguration']['Name'],
            bounds: {
              width: parseInt(
                p['VideoSourceConfiguration']['Bounds']['$']['width'],
                10
              ),
              height: parseInt(
                p['VideoSourceConfiguration']['Bounds']['$']['height'],
                10
              ),
              x: parseInt(
                p['VideoSourceConfiguration']['Bounds']['$']['x'],
                10
              ),
              y: parseInt(p['VideoSourceConfiguration']['Bounds']['$']['y'], 10)
            }
          }
        }
        if (p['VideoEncoderConfiguration']) {
          profile['video']['encoder'] = {
            token: p['VideoEncoderConfiguration']['$']['token'],
            name: p['VideoEncoderConfiguration']['Name'],
            resolution: {
              width: parseInt(
                p['VideoEncoderConfiguration']['Resolution']['Width'],
                10
              ),
              height: parseInt(
                p['VideoEncoderConfiguration']['Resolution']['Height'],
                10
              )
            },
            quality: parseInt(p['VideoEncoderConfiguration']['Quality'], 10),
            framerate: parseInt(
              p['VideoEncoderConfiguration']['RateControl']['FrameRateLimit'],
              10
            ),
            bitrate: parseInt(
              p['VideoEncoderConfiguration']['RateControl']['BitrateLimit'],
              10
            ),
            encoding: p['VideoEncoderConfiguration']['Encoding']
          }
        }
        if (p['AudioSourceConfiguration']) {
          profile['audio']['source'] = {
            token: p['AudioSourceConfiguration']['$']['token'],
            name: p['AudioSourceConfiguration']['Name']
          }
        }
        if (p['AudioEncoderConfiguration']) {
          profile['audio']['encoder'] = {
            token:
              '$' in p['AudioEncoderConfiguration']
                ? p['AudioEncoderConfiguration']['$']['token']
                : '',
            name: p['AudioEncoderConfiguration']['Name'],
            bitrate: parseInt(p['AudioEncoderConfiguration']['Bitrate'], 10),
            samplerate: parseInt(
              p['AudioEncoderConfiguration']['SampleRate'],
              10
            ),
            encoding: p['AudioEncoderConfiguration']['Encoding']
          }
        }
        if (p['PTZConfiguration']) {
          try {
            let r = p['PTZConfiguration']['PanTiltLimits']['Range']
            let xr = r['XRange']
            let x = profile['ptz']['range']['x']
            x['min'] = parseFloat(xr['Min'])
            x['max'] = parseFloat(xr['Max'])
          } catch (e) {}
          try {
            let r = p['PTZConfiguration']['PanTiltLimits']['Range']
            let yr = r['YRange']
            let y = profile['ptz']['range']['y']
            y['min'] = parseFloat(yr['Min'])
            y['max'] = parseFloat(yr['Max'])
          } catch (e) {}
          try {
            let r = p['PTZConfiguration']['ZoomLimits']['Range']
            let zr = r['XRange']
            let z = profile['ptz']['range']['z']
            z['min'] = parseFloat(zr['Min'])
            z['max'] = parseFloat(zr['Max'])
          } catch (e) {}
        }

        this.profile_list.push(profile)
        if (!this.current_profile) {
          this.current_profile = profile
        }
      })
      resolve()
    })
  })
  return promise
}

// Media::GetStreamURI (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetStreamURI = function () {
  let protocol_list = ['UDP', 'HTTP', 'RTSP']
  let promise = new Promise((resolve, reject) => {
    let profile_index = 0
    let protocol_index = 0
    let getStreamUri = () => {
      let profile = this.profile_list[profile_index]
      if (profile) {
        let protocol = protocol_list[protocol_index]
        if (protocol) {
          let token = profile['token']
          let params = {
            ProfileToken: token,
            Protocol: protocol
          }
          this.services.media.getStreamUri(params, (error, result) => {
            this.lastResponse = result
            if (!error) {
              let uri =
                result['data']['GetStreamUriResponse']['MediaUri']['Uri']
              uri = this._getUri(uri)
              this.profile_list[profile_index]['stream'][
                protocol.toLowerCase()
              ] = uri
            }
            protocol_index++
            getStreamUri()
          })
        } else {
          profile_index++
          protocol_index = 0
          getStreamUri()
        }
      } else {
        resolve()
        return
      }
    }
    getStreamUri()
  })
  return promise
}

// Media::GetSnapshotUri (Access Class: READ_MEDIA)
OnvifDevice.prototype._mediaGetSnapshotUri = function () {
  let promise = new Promise((resolve, reject) => {
    let profile_index = 0
    let getSnapshotUri = () => {
      let profile = this.profile_list[profile_index]
      if (profile) {
        let params = { ProfileToken: profile['token'] }
        this.services.media.getSnapshotUri(params, (error, result) => {
          this.lastResponse = result
          if (!error) {
            try {
              let snapshotUri =
                result['data']['GetSnapshotUriResponse']['MediaUri']['Uri']
              snapshotUri = this._getSnapshotUri(snapshotUri)
              profile['snapshot'] = snapshotUri
            } catch (e) {
              console.log(e)
            }
          }
          profile_index++
          getSnapshotUri()
        })
      } else {
        resolve()
      }
    }
    getSnapshotUri()
  })
  return promise
}

OnvifDevice.prototype._getXaddr = function (directXaddr) {
  if (!this.keepAddr) return directXaddr
  const path = mUrl.parse(directXaddr).path
  return 'http://' + this.address + path
}

OnvifDevice.prototype._getUri = function (directUri) {
  if (typeof directUri === 'object' && directUri['_']) {
    directUri = directUri['_']
  }
  if (!this.keepAddr) return directUri
  const base = mUrl.parse('http://' + this.address)
  const parts = mUrl.parse(directUri)
  const newParts = {
    host: base.host,
    pathname: base.pathname + parts.pathname
  }
  const newUri = mUrl.format(newParts)
  return newUri
}

OnvifDevice.prototype._getSnapshotUri = function (directUri) {
  if (typeof directUri === 'object' && directUri['_']) {
    directUri = directUri['_']
  }
  if (!this.keepAddr) return directUri
  const base = mUrl.parse('http://' + this.address)
  const parts = mUrl.parse(directUri)
  const newParts = {
    protocol: parts.protocol,
    host: base.host,
    pathname: base.pathname + parts.pathname
  }
  const newUri = mUrl.format(newParts)
  return newUri
}

module.exports = OnvifDevice
